//Copyright (C) 2012 Chirila Alexandru
//This file is part of Robo-GoldMiner
//
//Robo-GoldMiner is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//Robo-GoldMiner is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with Robo-GoldMiner.  If not, see <http://www.gnu.org/licenses/>.

package gold;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * Provides a simple shell interface for the user to play the game
 * @author Alexandru Chirila
 *
 */
public class View implements Serializable {

	private static final long serialVersionUID = -5741266357624919409L;
	private int sleepTime;
	private boolean quiet = false;
	private Maze game = null;
	private StackCommands cmdStack = new StackCommands();
	private Map<String, Command> commands = new HashMap<String, Command>();

	/**
	 * Send a string as an ANSI escape sequence.
	 * <br />
	 * Example: View.escape("0m")
	 * <br />
	 * Result: prints "\033[0m"
	 * @param str
	 */
	public static void escape(String str) {
		String ESC = "\033[";
		System.out.print(ESC + str);
		System.out.flush();
	}
	
	/**
	 * Normal print, but also reset the ANSI colors by escaping "0m" 
	 * @param str the string
	 */
	public static void out(String str) {
		escape("0m");
		System.out.print(str);
	}

	/**
	 * Prints with color. (black,red,green,yellow,blue,magenta,cyan,white)
	 * @param str the string 
	 * @param colorFg foreground color
	 * @param colorBg background color
	 */
	public static void out(String str, String colorFg, String colorBg) {
		Map<String, Integer> colorMap = new HashMap<String, Integer>();
		colorMap.put("black", 0);
		colorMap.put("red", 1);
		colorMap.put("green", 2);
		colorMap.put("yellow", 3);
		colorMap.put("blue", 4);
		colorMap.put("magenta", 5);
		colorMap.put("cyan", 6);
		colorMap.put("white", 7);
		colorMap.put(null, 9);
		escape("3" + colorMap.get(colorFg) + "m");
		escape("4" + colorMap.get(colorBg) + "m");
		System.out.print(str);
	}

	/**
	 * Prints with color. (black,red,green,yellow,blue,magenta,cyan,white)
	 * @param str the string 
	 * @param colorFg foreground color
	 * @param colorBg background color
	 */
	public static void out(String str, int colorFg, int colorBg) {
		escape("3" + colorFg + "m");
		escape("4" + colorBg + "m");
		System.out.print(str);
	}

	/**
	 * Escapes a new page in the terminal and places the cursor in top right corner
	 */
	public static void newPage() {
		escape("2J");
		escape("0;0H");
		escape("Z");
	}

	public View() {

	}

	public void runGame() {

		String showError = new String();

		while (true) {
			try {

				System.out.println(showError);
				printMenu();
				if (dispatch(listen("Enter choice: ")) == 0)
					break;
				showError = "";

			} catch (IOException e) {
				showError = e.getMessage();
			} catch (ParseException e) {
				showError = e.getMessage();
			} catch (EndGame e) {
				showError = e.getMessage();
			} catch (ClassNotFoundException e) {
				showError = e.getMessage();
			}
		}
	}

	private void openGame(String file) throws IOException, ParseException {
		game = new Maze();
		cmdStack = new StackCommands();
		game.open(file, this);

	}

	private void printMenu() {
		StringBuilder sb = new StringBuilder();

		sb.append("1 - Open new game\n");
		sb.append("2 - Run command script\n");
		sb.append("3 - Save current game\n");
		sb.append("4 - Load saved game\n");
		sb.append("5 - Undo/Redo interface\n");
		sb.append("0 - Exit\n");

		System.out.println(sb.toString());

	}

	private String listen(String msg) throws IOException {
		InputStreamReader in = new InputStreamReader(System.in);
		BufferedReader buf = new BufferedReader(in);
		System.out.print(msg);
		return buf.readLine();
	}

	private int dispatch(String line) throws IOException, ParseException,
			EndGame, ClassNotFoundException {
		switch (Integer.parseInt(line)) {
		case 0:
			return 0;
		case 1:
			openGame(listen("Enter game file: "));
			return 1;

		case 2:

			clearCommands();
			game = Maze.reset();

			runCommands(listen("Enter script file: "));
			return 2;

		case 3:

			StackCommands.saveStack(cmdStack, listen("Enter stack file: "));
			return 3;

		case 4:
			cmdStack = StackCommands.loadStack(listen("Enter stack file: "));
			cmdStack.runStack();
			return 4;

		case 5:
			stepUndoRedoInterface();
			return 5;

		default:
			return 0;
		}
	}

	private void stepUndoRedoInterface() throws IOException, ParseException,
			EndGame {
		System.out.println("Enter `u` to undo `r` to redo and `e` to exit: ");
		String line;

		while (true) {
			line = listen("Enter command:");
			if (line.equalsIgnoreCase("u")) {
				if (!cmdStack.stepUndo()) {
					System.out.println("No more commands in stack to undo");
				}
			} else if (line.equalsIgnoreCase("r")) {
				if (!cmdStack.stepRedo()) {
					System.out.println("No more commands in stack to redo");
				}
			} else
				break;
		}
	}

	private void getFunctionTable(String file) throws IOException,
			FileNotFoundException, ParseException {

		int lineNumber = 0;

		StringTokenizer st;
		String line;
		String token;

		InputStreamReader in = new InputStreamReader(new FileInputStream(file));
		BufferedReader buf = new BufferedReader(in);

		try {
			while ((line = buf.readLine()) != null) {

				lineNumber++;
				st = new StringTokenizer(line, " \t\n\r\f;");

				if (!st.hasMoreTokens())
					continue;

				token = st.nextToken();

				if (token.equals("define")) {
					if (st.hasMoreTokens()) {
						Function f = new Function(game, cmdStack);
						f.setName(st.nextToken());
						commands.put(f.getName(), f);
					} else {
						throw new ParseException(
								"Error parsing expected name after define (line : "
										+ lineNumber + " )\n\t" + line);
					}
				}
			}
		} finally {
			in.close();
		}
	}

	private void clearCommands() {
		commands.clear();
		cmdStack.clearAll();
	}

	private void runCommands(String file) throws ParseException, IOException,
			EndGame {

		boolean funcDef = false;

		int lineNumber = 0;

		StringTokenizer st;
		String line = new String();
		String token = new String();

		getFunctionTable(file); // Get functions headers first

		InputStreamReader in = new InputStreamReader(new FileInputStream(file));
		BufferedReader buf = new BufferedReader(in);

		Function currentFunction = null;
		Command currentCommand = null;

		
		//default values
		sleepTime = 250;
		quiet = false;

		try {
			while ((line = buf.readLine()) != null) {

				lineNumber++;
				st = new StringTokenizer(line, " \t\n\r\f;");

				// Parsing the line
				if (st.hasMoreTokens())
					token = st.nextToken();
				else
					continue; // white line

				if (token.startsWith("#")) // comments
					continue;

				if (token.equals("set")) {
					if (!st.hasMoreTokens())
						throw new ParseException(
								"Error parsing expected token after set");
					token = st.nextToken();
					if (token.equals("timeout")) {
						
						if (!st.hasMoreTokens())
							throw new ParseException(
									"Error parsing expected number after timeout");
						
						token = st.nextToken();
						
						if (!token.matches("[0-9]+"))
							throw new ParseException(
									"Error parsing expected number after timeout");
						
						sleepTime = Integer.parseInt(token);
						
						continue;
					}
					
					if (token.equals("quiet")){
						quiet = true;
						continue;
					}
					
				}

				if (token.equals("define")) {
					// the script define a function here
					if (funcDef)
						throw new ParseException(
								"You cannot define a function within a function");

					token = st.nextToken(); // no need for test we test it in
											// getFunctionTable
					currentFunction = (Function) commands.get(token);
					funcDef = true; // start function defining
					continue;
				}

				if (token.equals("end")) {
					// the scripts marks the end of a functions body
					funcDef = false;
					continue;
				}

				// Parsing actual commands
				if (token.equals("color")) {
					// changing color command
					if (!st.hasMoreTokens())
						throw new ParseException(
								"Error parsing expected color name after color");
					token = st.nextToken();
					currentCommand = new CommandColor(game);
					((CommandColor) currentCommand).setColor(token);

				} else if (token.equals("on")) {
					// conditional command
					if (!st.hasMoreTokens())
						throw new ParseException(
								"Error parsing expected color name after on");

					token = st.nextToken();
					currentCommand = new CommandOn(game);
					((CommandOn) currentCommand).setColor(token);

					// primitives
				} else if (token.equals("move")) {
					currentCommand = new CommandMove(game);
				} else if (token.equals("turnCW")) {
					currentCommand = new CommandTurnCW(game);
				} else if (token.equals("turnCCW")) {
					currentCommand = new CommandTurnCCW(game);

				} else {
					// functions
					currentCommand = commands.get(token);
				}

				if (currentCommand == null)
					throw new ParseException("Error parsing token `" + token
							+ "`.");

				// Executing or adding commands to functions

				if (funcDef)
					currentFunction.addCommand(currentCommand);
				else
					cmdStack.execute(currentCommand);
			}
			throw new EndGame("You lose! No more moves");
		} catch (ParseException p) {
			throw new ParseException(p.getMessage() + "(line : " + lineNumber
					+ " )\n\t" + line);
		} finally {
			in.close();
		}
	}

	private void writeObject(ObjectOutputStream stream) throws IOException {
		stream.defaultWriteObject();
	}

	private void readObject(ObjectInputStream stream) throws IOException,
			ClassNotFoundException {
		stream.defaultReadObject();
	}

	public void update(String str) {
		
		if (!quiet)
			newPage();

		View.out(str + "\n");

		if (!quiet)
			game.draw();

		try {
			Thread.sleep(sleepTime);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}
